﻿/*
Copyright (C) 2009  Tomasz Chrzanowski

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>
*/


using System;
using System.Collections.Generic;
using System.Data;
using System.Data.SqlClient;
using System.Threading;
using System.Windows.Forms;
using Microsoft.Win32;

namespace Com.TomCdc.ChangeDataCapture
{
    public partial class DatabaseConnection : Form
    {
        /// <summary>
        /// Describes context state of the application.
        /// </summary>
        enum UiContextState
        {
            NotTested,
            Testing,
            Tested,
            Connected,
            Closed
        }

        /// <summary>
        /// The collection of servers and databased used in the past. Feeded from system registry.
        /// </summary>
        private Dictionary<string, string> _dbServerHistory;

        /// <summary>
        /// The key name in the system registry used by this CDC module.
        /// </summary>
        private static string CDC_MANAGER_REG_KEY = "CdcManager";

        /// <summary>
        /// The thread used to asyncronously test sql connection
        /// </summary>
        private Thread _sqlConnTesterThread;

        /// <summary>
        /// The id of the test being currently performed. The id is increased by one every time new test released.
        /// </summary>
        private object _sqlTestRequestId = 0;
        
        /// <summary>
        /// Represents a delegate that is declared voide and takes two parameters.
        /// </summary>
        /// <param name="reqId">The id of a test.</param>
        /// <param name="result">The result of a test.</param>
        private delegate void TestFinalizerInvoker(int reqId, object result);

        /// <summary>
        /// The state of application, mainly used to enable/disable user controls.
        /// </summary>
        private UiContextState _contextStatus;

        /// <summary>
        /// The sql query selecting all server databases.
        /// </summary>
        private const string SQL_SELECT_DATABASES = "select name from master.dbo.sysdatabases";

        /// <summary>
        /// The sql connection builder storing tested connection to database server.
        /// </summary>
        public SqlConnectionStringBuilder TestedConnection
        {
            get
            {
                return _connectionBuilder;
            }
        }
        private SqlConnectionStringBuilder _connectionBuilder;
        
        /// <summary>
        /// Initializes a new instance of <see cref="DatabaseConnection"/> class.
        /// </summary>
        public DatabaseConnection()
        {
            // initializes UI controls
            InitializeComponent();

            // start status of app NotTested
            _contextStatus = UiContextState.NotTested;
            _dbServerHistory = new Dictionary<string, string>();

            // default result cancel, if succesfully connected checged to OK
            DialogResult = DialogResult.Cancel;
            
            // sets integrated authentication as default 
            uxAuthentication.SelectedIndex = 0;
            
            // gets historical data from system registry and populates UI controls
            FillDbServersHistory();

            // sets UI controls according to current context status
            EnableDisableUIControls();
        }

        /// <summary>
        /// Retrievs sql servers and databases used in the past, from the system registry.
        /// Then adds them to servers list and dictionary data structure for later use.
        /// </summary>
        private void FillDbServersHistory()
        {
            // gets registry subkey
            RegistryKey regKey = CommonHelper.GetRegUserKey(CDC_MANAGER_REG_KEY + "\\DbServersHistory", false);
            if (regKey != null)
            {
                // adds do combo box and dictionary all entries in opened subkey
                foreach (string name in regKey.GetValueNames())
                {
                    _dbServerHistory.Add(name, (string)regKey.GetValue(name));
                    uxServerName.Items.Add(name);
                }
                regKey.Close();
            }
        }

        /// <summary>
        /// Changes context status and sets form controls availability.
        /// </summary>
        private void FormDataChanged()
        {
            // when any data affecting connection to server changed change status to not tested
            _contextStatus = UiContextState.NotTested;

            // reset currently selected database
            uxDatabaseName.SelectedIndex = -1;

            // set form controls availability
            EnableDisableUIControls();
        }

        /// <summary>
        /// Collects data filled out by the user into connection builder object.
        /// </summary>
        /// <returns>The sql connection builder object populated with the data from the form.</returns>
        private SqlConnectionStringBuilder BuildConnection()
        {
            SqlConnectionStringBuilder conn = new SqlConnectionStringBuilder();
            conn.ConnectTimeout = 5;
            conn.DataSource = uxServerName.Text;
            conn.InitialCatalog = uxDatabaseName.Text;
            if (uxAuthentication.SelectedIndex == 0)
            {
                conn.IntegratedSecurity = true;
            }
            else
            {
                conn.IntegratedSecurity = false;
                conn.UserID = uxLogin.Text;
                conn.Password = uxPassword.Text;
            }

            return conn;
        }

        /// <summary>
        /// Prepares enviroment and run asyncronous sql connection test.
        /// </summary>
        private void PerformAsyncSqlConnTest()
        {
            _contextStatus = UiContextState.Testing;
            
            // clears database names list, to be filled after successful test again
            uxDatabaseName.Items.Clear();
            
            // sets form controls availability
            EnableDisableUIControls();

            _sqlConnTesterThread = new Thread(new ThreadStart(TestSqlConnection));
            
            // collects data from the form and build sql connection
            _connectionBuilder = BuildConnection();

            // starts testing thread
            _sqlConnTesterThread.Start();
        }

        /// <summary>
        /// Performs sql connection testing. After finishing invokes method back in user thread to
        /// present the results.
        /// </summary>
        private void TestSqlConnection()
        {
            // id of the test performed in this method call
            int reqId;
            lock (_sqlTestRequestId)
            {
                // increases current test id and assign it to global and local variable
                _sqlTestRequestId = reqId = (int)_sqlTestRequestId + 1;
            }

            object result = _connectionBuilder;

            // prepares and test sql connection
            SqlConnection sqlTestConn 
                = new SqlConnection(((SqlConnectionStringBuilder)result).ConnectionString);
            try
            {
                sqlTestConn.Open();
            }
            catch (Exception ex)
            {
                // if test failed result value is the Exception type
                result = ex;
            }
            
            // checks if result is expected, when form closed and testing thread still runing, result 
            // cannot be processed
            if (_contextStatus == UiContextState.Testing)
            {
                // to finalizing method test id and result object is passed, test id needed to verify if
                // this test call is the one we expect (might have been canceled in the mean time)
                // result is going to be SqlStringBuilder or Exception object which informs of the test result
                BeginInvoke(new TestFinalizerInvoker(FinishAsyncSqlConnTest), new object[] { reqId, result });
            }
        }

        /// <summary>
        /// Finishes asyncronous sql connection test, by assgning tested parameters to <see cref="_connectionBuilder"/>
        /// class member. Before that check is performed to determined whether method is called from currently active
        /// test ID.</summary>
        /// <param name="reqId">The test ID which has finished.</param>
        /// <param name="result">The test result. Can be Exception object if test failed otherwise
        /// SqlConnectionBuilder.</param>
        private void FinishAsyncSqlConnTest(int reqId, object result)
        {
            // locks _sqlTestRequestId object so no potential new test can modify it
            lock (_sqlTestRequestId)
            {
                // if not equal means the method call comes from not current test
                if (reqId != (int)_sqlTestRequestId) return;

                // checks if test was successful
                if (result is SqlConnectionStringBuilder)
                {
                    // test successful, assigns tested connection to _connectionBuilder class member
                    // exposed by property TestedConnection
                    _connectionBuilder = (SqlConnectionStringBuilder)result;
                    
                    // gets all database names from tested server
                    PopulateDatabaseasList();

                    _contextStatus = UiContextState.Tested;
                }
                else
                {
                    // test failed, reset connection builder and set context status
                    _connectionBuilder = null;
                    _contextStatus = UiContextState.NotTested;
                }
            }

            // if true means the test failed
            if (_connectionBuilder == null)
            {
                MessageBox.Show("Connaction failed." + Environment.NewLine + Environment.NewLine +
                    ((Exception)result).Message, "Connection failed.", MessageBoxButtons.OK, MessageBoxIcon.Error);
            }

            // sets form controls availability
            EnableDisableUIControls();
        }

        /// <summary>
        /// Populates list of database names of selected server. If db server was used in the past, last used
        /// database is selected.
        /// </summary>
        private void PopulateDatabaseasList()
        {
            string lastSelDbName = "";
            // checks if database was used in the past
            if (_dbServerHistory.ContainsKey(_connectionBuilder.DataSource))
            {
                // finds recently used database
                lastSelDbName = _dbServerHistory[_connectionBuilder.DataSource];
            }

            // connects to chosen server and selects all databases
            using (SqlConnection sqlConn = new SqlConnection(_connectionBuilder.ConnectionString))
            {
                SqlCommand sqlCmd = new SqlCommand(SQL_SELECT_DATABASES, sqlConn);
                sqlConn.Open();
                SqlDataReader sqlReader = sqlCmd.ExecuteReader();
                
                // adds database name to combo box
                while (sqlReader.Read())
                {
                    string dbName = sqlReader.GetString(0);
                    uxDatabaseName.Items.Add(dbName);
                    
                    // checks if added database name is the one which was used previously
                    if (dbName == lastSelDbName)
                    {
                        // selects previously used database
                        uxDatabaseName.SelectedIndex = uxDatabaseName.Items.Count - 1;
                    }
                }
            }
        }


        /// <summary>
        /// Sets availability of user interface controls, depending on the current application status.
        /// </summary>
        private void EnableDisableUIControls()
        {
           
            if (_contextStatus == UiContextState.Tested)
            {
                uxServerName.Enabled = true;
                uxAuthentication.Enabled = true;
                
                // checks autentication type, when integrated, disable user name and password
                if (uxAuthentication.SelectedIndex == 0)
                {
                    uxLogin.Enabled = false;
                    uxPassword.Enabled = false;
                }
                else
                {
                    uxLogin.Enabled = true;
                    uxPassword.Enabled = true;
                } 
                uxDatabaseName.Enabled = true;

                // checks if database name selected
                if (uxDatabaseName.SelectedIndex > -1)
                {
                    uxConnect.Enabled = true;
                }
                else
                {
                    uxConnect.Enabled = false;
                }
                uxCancel.Enabled = true;
                uxTestConnection.Enabled = false;
            }

            if (_contextStatus == UiContextState.NotTested )
            {
                uxServerName.Enabled = true;
                uxAuthentication.Enabled = true;

                // checks autentication type, when integrated, disable user name and password
                if (uxAuthentication.SelectedIndex == 0)
                {
                    uxLogin.Enabled = false;
                    uxPassword.Enabled = false;
                }
                else
                {
                    uxLogin.Enabled = true;
                    uxPassword.Enabled = true;
                }
                uxDatabaseName.Enabled = false;
                uxConnect.Enabled = false;
                uxCancel.Enabled = true;
                uxTestConnection.Enabled = true;
            }

            if (_contextStatus == UiContextState.Testing)
            {
                uxServerName.Enabled = false;
                uxAuthentication.Enabled = false;
                uxLogin.Enabled = false;
                uxPassword.Enabled = false;
                uxDatabaseName.Enabled = false;
                uxConnect.Enabled = false;
                uxCancel.Enabled = true;
                uxTestConnection.Enabled = false;
            }

        }

        /// <summary>
        /// Saves server and database name to system registry.
        /// </summary>
        private void SaveConnectionToRegistry()
        {
            // gets subkey DbServersHistory of current aplication's key
            RegistryKey regKey = CommonHelper.GetRegUserKey(CDC_MANAGER_REG_KEY + "\\DbServersHistory", true);
            if (regKey != null)
            {
                regKey.SetValue(_connectionBuilder.DataSource, _connectionBuilder.InitialCatalog);
                regKey.Close();
            }
        }

        #region Even Handlers

        private void uxCancel_Click(object sender, EventArgs e)
        {
            // destroys any existing connection
            _connectionBuilder = null;

            // checks if testing in progress
            if (_contextStatus == UiContextState.Testing)
            {
                // increases test id so any test in progress will be out of date
                lock (_sqlTestRequestId)
                {
                    _sqlTestRequestId = (int)_sqlTestRequestId + 1;
                }

                // sets status of context
                _contextStatus = UiContextState.NotTested;
                
                // enable/disable controls according to context status
                EnableDisableUIControls();
            }
            else
            {
                // closes form if no testing in progress and cancel pressed
                Close();
            }
        }

        private void uxTestConnection_Click(object sender, EventArgs e)
        {
            if (string.IsNullOrEmpty(uxServerName.Text))
            {
                MessageBox.Show("No server name has been entered.", "Server name",
                    MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
            }
            else
            {
                PerformAsyncSqlConnTest();
            }
        }

        private void uxConnect_Click(object sender, EventArgs e)
        {
            DialogResult = DialogResult.OK;
            _contextStatus = UiContextState.Connected;
            SaveConnectionToRegistry();
            Close();
        }

        private void uxDatabaseName_SelectedIndexChanged(object sender, EventArgs e)
        {
            // if database selection changed assign new value to connection object
            if (uxDatabaseName.SelectedIndex > -1)
            {
                _connectionBuilder.InitialCatalog = uxDatabaseName.SelectedItem.ToString();
            }

            EnableDisableUIControls();
        }

        private void uxAuthentication_SelectedIndexChanged(object sender, EventArgs e)
        {
            // if integrated authentication selected, clear out login and password
            if (uxAuthentication.SelectedIndex == 0)
            {
                uxLogin.Text = "";
                uxPassword.Text = "";
            }
            FormDataChanged();
        }

        private void uxServerName_TextChanged(object sender, EventArgs e)
        {
            FormDataChanged();
        }

        private void uxLogin_SelectedIndexChanged(object sender, EventArgs e)
        {
            FormDataChanged();
        }

        private void uxLogin_TextChanged(object sender, EventArgs e)
        {
            FormDataChanged();
        }

        private void uxPassword_TextChanged(object sender, EventArgs e)
        {
            FormDataChanged();
        }


        private void DatabaseConnection_FormClosed(object sender, FormClosedEventArgs e)
        {
            // if form closed without connecting, connection object destroyed
            if (!(_contextStatus == UiContextState.Connected))
            {
                _connectionBuilder = null;
            }
            _contextStatus = UiContextState.Closed;
        }

        #endregion

        private void uxServerName_KeyDown(object sender, KeyEventArgs e)
        {
            // spaces not allowed in server name
            if (e.KeyCode == Keys.Space)
            {
                e.SuppressKeyPress = true;
            }
        }

    }
}
